
import { appoxEnums } from './appox'
import { agencyAsync } from './appox'
import { appoxCallback } from './appox'
import { appoxUtility } from './appox'
import { AppoxSessionIdentifier } from './appox'
import { AppoxBusProtocolFileID } from './appox'
import { AppoxGeneralSample } from './appox'

/**
 * 桥接bi-common.js和appox.js的功能
 * 关联的其他文件(需放于HTML模块所在根路径下)包括：
 * - index.html
 * - js/bi-bridge-client.js
 * - css/global.css
 */
var biBridge = new BIBridge();
function BIBridge() {

    /**
     * 获取bi-bridge.js版本
     * @returns bi-bridge.js版本
     */
    this.getVersion = () => {
        return "2.1.1"; // 修正pyTemplateNames传输
    }

    /**
     * 初始化模块
     * @param {String} htmlModulesRootPath HTML模块文件所在根路径
     * @param {String} entryName 模块入口名称
     * @param {String} viewConfig 模块视图配置
     * @param {Array} relatedModules String数组：相关模块ID列表
     * @returns {BIHtmlModuleInstanceInfo} 模块实例信息
     */
    this.initializeModule = async (htmlModulesRootPath, entryName, viewConfig, showHelp) => {
        this.releaseModule();

        biBI.currentID = biBI.currentID + 1;
        let bridgeID = biBI.currentID.toString();

        let webStyleWidth = 650;
        let webStyleHeight = 500;
        let relatedModuleList = [];
        try {
            let response = await fetch(`${htmlModulesRootPath}/${entryName}.xml`);
            let text = await response.text()
            let parser = new DOMParser()
            let xmlDoc = parser.parseFromString(text, 'text/xml')
            webStyleWidth = parseInt(xmlDoc.querySelector('root').getAttribute('web_style_width') || '650')
            webStyleHeight = parseInt(xmlDoc.querySelector('root').getAttribute('web_style_height') || '500')
            let rmNodes = xmlDoc.querySelectorAll('rm')
            relatedModuleList = Array.from(rmNodes).map(node => node.textContent.trim())
        } catch (error) {
            console.error('Failed to load module XML:', error)
            return null;
        }

        biBI.bridgeCtxs[bridgeID] = new BIBridgeContext(bridgeID, entryName, viewConfig, relatedModuleList, showHelp);
        if (biBI.timerID == null) biBI.timerID = setInterval(biBI.onTimerTick, 10);
        return new BIHtmlModuleInstanceInfo(bridgeID, webStyleWidth, webStyleHeight, relatedModuleList);
    };

    /**
     * 注册模块绑定的iframe内容窗口
     * @param {String} bridgeId 桥接ID
     * @param {Window} contentWindow iframe内容窗口
     */
    this.registerModuleContentWindow = (bridgeId, contentWindow) => {
        if (biBI.bridgeCtxs[bridgeId] == null) return;
        biBI.bridgeCtxs[bridgeId].contentWindow = contentWindow;
    }

    /**
     * 处理主窗口收到的消息
     * @param {Any} msgData 消息数据
     */
    this.onWindowMessage = (msgData) => {
        biBI.handleWindowMessage(msgData);
    }

    /**
     * 重置数据
     */
    this.onResetData = () => {
        biBI.handleResetData();
    }

    /**
     * 输入通用样本
     * @param {AppoxGeneralSample} sample 通用样本
     */
    this.onInputGeneralSample = (sample) => {
        biBI.handleInputGeneralSample(sample);
    }

    /**
     * 释放模块
     * @returns 模块视图配置
     */
    this.releaseModule = (bridgeId) => {
        if (biBI.bridgeCtxs[bridgeId] == null) return null;
        let config = biBI.bridgeCtxs[bridgeId].viewConfig;
        delete biBI.bridgeCtxs[bridgeId];
        return config;
    };
}
export { biBridge };

/**
 * HTML模块实例信息
 * @param {String} bridgeId 桥接ID
 * @param {Number} webStyleWidth 页面宽度
 * @param {Number} webStyleHeight 页面高度
 */
function BIHtmlModuleInstanceInfo(bridgeId, webStyleWidth, webStyleHeight) {
    this.bridgeId = bridgeId;
    this.webStyleWidth = webStyleWidth;
    this.webStyleHeight = webStyleHeight;
}
export { BIHtmlModuleInstanceInfo };

/**
 * bi-bridge.js的内部实现
 */
var biBI = new BIBridgeInternal();
function BIBridgeInternal() {
    
    this.currentID = 0;
    this.timerID = null;
    this.appID = null;
    this.bufferRange = null;
    this.mtNames = null;
    this.pluginVers = {};
    this.systemStatus = {};
    this.pyTemplateNames = {};
    this.isOnline = null;
    this.bufferRangeLower = null;
    this.bufferRangeUpper = null;
    this.dataPath = null;
    this.bridgeCtxs = {};

    this.queryRelatedStates = () => {
        let received = 0;
        let target = 8 + Object.keys(appoxEnums.SystemStatus).length + Object.keys(appoxEnums.NativeLibraryType).length;
        let result = [ false, 0 ]; // [ isRunning, interest ]
        return new Promise((resolve) => {
            agencyAsync.getAppID().then((id) => {
                this.appID = id;
                if (++received == target) resolve(result);
            });
            agencyAsync.getBufferRange().then((range) => {
                this.bufferRange = range;
                if (++received == target) resolve(result);
            });
            agencyAsync.getManualTriggerNames().then((names) => {
                this.mtNames = names;
                if (++received == target) resolve(result);
            });
            agencyAsync.getGlobalVariable("Python.TemplateList", "").then(async (nameTexts) => {
                let templateIDs = nameTexts == null || nameTexts.length == 0 ? [] : nameTexts.split("\n");
                for (let templateID of templateIDs) {
                    if (this.pyTemplateNames[templateID] != null) continue;
                    let templateInfo = await agencyAsync.getGlobalVariable("Python.Template." + templateID, "");
                    this.pyTemplateNames[templateID] = this.parseTemplateName(templateInfo) || templateID;
                }
                if (++received == target) resolve(result);
            });
            agencyAsync.getAppMode().then((mode) => {
                this.isOnline = mode != null && mode == appoxEnums.ApplicationMode.Online;
                if (++received == target) resolve(result);
            });
            agencyAsync.getAppStatus().then((status) => {
                result[0] = status != null && (status == appoxEnums.ApplicationStatus.Starting || status == appoxEnums.ApplicationStatus.Running);
                if (++received == target) resolve(result);
            });
            agencyAsync.getInterestTime().then((time) => {
                result[1] = time == null ? 0.0 : time;
                if (++received == target) resolve(result);
            });
            agencyAsync.getGlobalParameter("System.DataPath", null).then((path) => {
                this.dataPath = path;
                if (++received == target) resolve(result);
            });
            Object.keys(appoxEnums.SystemStatus).forEach((s) => {
                agencyAsync.getSystemStatus(s).then((text) => {
                    this.systemStatus[s] = text == null ? "" : text;
                    if (++received == target) resolve(result);
                });
            });
            Object.keys(appoxEnums.NativeLibraryType).forEach((type) => {
                agencyAsync.getNativePluginVersions(type).then((vers) => {
                    this.pluginVers[type] = vers == null ? {} : vers;
                    if (++received == target) resolve(result);
                });
            });
        });
    };

    this.onTimerTick = () => {

        if (this.bridgeCtxs.length == 0) return;

        this.queryRelatedStates().then((result) => {
            let isRunning = result[0];
            let interest = result[1];
            for (let bridgeId in this.bridgeCtxs) {
                this.bridgeCtxs[bridgeId].isRunningUpdate = isRunning;
                this.bridgeCtxs[bridgeId].interestUpdate = interest;
            }
        });
        
        if (this.bufferRange != null) {
            agencyAsync.convertTimeIntoSession(this.bufferRange.begin).then((result) => {
                this.bufferRangeLower = result;
            });
            agencyAsync.convertTimeIntoSession(this.bufferRange.end).then((result) => {
                this.bufferRangeUpper = result;
            });
        }

        for (let bridgeId in this.bridgeCtxs) {
            this.handleBridgeCtx(this.bridgeCtxs[bridgeId]);
        }
    };

    this.signalIDToSignalInfoComps = (signalID) => {
        return new Promise((resolve) => {
            agencyAsync.getSignalName(signalID, true).then((fullName) => {
                let infoComps = signalID.split(":");
                let typeName = fullName == null ? infoComps[1] : fullName.split(":")[0];
                resolve([ signalID, infoComps[0], infoComps[1], typeName, infoComps[2] ]);
            });
        });
    };

    this.parseTemplateName = (templateInfo) => {
        try {
            const parser = new DOMParser();
            const xmlDoc = parser.parseFromString(templateInfo, "text/xml");
            if (xmlDoc.getElementsByTagName("parsererror").length > 0) return null;

            const templateElement = xmlDoc.getElementsByTagName("python_template")[0];
            if (!templateElement) return null;
  
            if (appoxCallback.onGetAppLanguage() == appoxEnums.Language.Chinese) {
                return templateElement.getAttribute("name_zh") || templateElement.getAttribute("name") ;
            } else {
                return templateElement.getAttribute("name");
            }
        }
        catch (error) {
            return null;
        }
    };

    this.sendBridgeMessage = (bridgeCtx, funcId, params) => {
        let comps = [];
        comps.push("BIBridgeMessage");
        comps.push(bridgeCtx.bridgeId);
        comps.push(funcId);
        for (let i = 0; i < params.length; i++) {
            comps.push(params[i]);
        }
        bridgeCtx.contentWindow.postMessage(comps);
    };

    this.sendBridgeMessageSafe = (bridgeCtx, funcId, params) => {
        if (bridgeCtx.contentWindow == null || !bridgeCtx.clientInitialized || !bridgeCtx.initArgsSent) return;
        this.sendBridgeMessage(bridgeCtx, funcId, params);
    };

    this.handleBridgeCtx = (bridgeCtx) => {

        if (bridgeCtx.relatedModuleConfigs == null && !bridgeCtx.queryingRelatedModuleConfigs) {
            if (bridgeCtx.relatedModuleList.length == 0) {
                bridgeCtx.relatedModuleConfigs = {};
            }
            else {
                bridgeCtx.queryingRelatedModuleConfigs = true;
                let received = 0;
                let target = bridgeCtx.relatedModuleList.length;
                let result = {};
                bridgeCtx.relatedModuleList.forEach((id) => {
                    agencyAsync.getModuleConfig(null, id).then((config) => {
                        result[id] = config;
                        if (++received == target) {
                            bridgeCtx.relatedModuleConfigs = result;
                            bridgeCtx.queryingRelatedModuleConfigs = false;
                        }
                    });
                });
            }
        }

        if (bridgeCtx.contentWindow == null || !bridgeCtx.clientInitialized) return;

        let stateParams = [];
        stateParams.push(bridgeCtx.entryName);
        stateParams.push(appoxCallback.onGetAppLanguage() == appoxEnums.Language.Chinese);
        stateParams.push(this.isOnline == null ? false : this.isOnline);
        stateParams.push(this.dataPath);
        stateParams.push(this.mtNames);
        stateParams.push(this.pluginVers[appoxEnums.NativeLibraryType.Native]);
        stateParams.push(this.pluginVers[appoxEnums.NativeLibraryType.Bus]);
        stateParams.push(this.pluginVers[appoxEnums.NativeLibraryType.Video]);
        stateParams.push(this.pluginVers[appoxEnums.NativeLibraryType.Device]);
        stateParams.push(this.pluginVers[appoxEnums.NativeLibraryType.Processor]);
        stateParams.push(this.pluginVers[appoxEnums.NativeLibraryType.FileIO]);
        stateParams.push(this.systemStatus);
        stateParams.push(this.pyTemplateNames);
        stateParams.push(appoxCallback.onCheckPreferPRCWeb());
        stateParams.push(this.appID);
        if (this.bufferRangeLower == null || this.bufferRangeUpper == null) {
            stateParams.push(null);
        }
        else {
            stateParams.push([this.bufferRangeLower.session.toDate(), this.bufferRangeLower.time, this.bufferRangeUpper.session.toDate(), this.bufferRangeUpper.time]);
        }
        this.sendBridgeMessage(bridgeCtx, "SystemState", stateParams);

        if (!bridgeCtx.initArgsSent && bridgeCtx.relatedModuleConfigs != null) {
            let initParams = [];
            initParams.push(bridgeCtx.viewConfig);
            initParams.push(bridgeCtx.relatedModuleConfigs);
            initParams.push(bridgeCtx.showHelp);
            this.sendBridgeMessage(bridgeCtx, "InitArgs", initParams);
            bridgeCtx.initArgsSent = true;
        }

        if (bridgeCtx.isRunning == null) {
            if (bridgeCtx.isRunningUpdate != null) {
                bridgeCtx.isRunning = bridgeCtx.isRunningUpdate;
                if (bridgeCtx.isRunning) this.sendBridgeMessageSafe(bridgeCtx, "StartSession", []);
            }
        }
        else if (bridgeCtx.isRunning != bridgeCtx.isRunningUpdate) {
            bridgeCtx.isRunning = bridgeCtx.isRunningUpdate;
            if (bridgeCtx.isRunning) this.sendBridgeMessageSafe(bridgeCtx, "StartSession", []);
            else this.sendBridgeMessageSafe(bridgeCtx, "StopSession", []);
        }
        if ((bridgeCtx.interest == null && bridgeCtx.interestUpdate != null) || 
            (bridgeCtx.interest != null && bridgeCtx.interest != bridgeCtx.interestUpdate)) {
            bridgeCtx.interest = bridgeCtx.interestUpdate;
            agencyAsync.convertTimeIntoSession(bridgeCtx.interest).then((result) => {
                if (result == null || result.session == null) return;
                this.sendBridgeMessageSafe(bridgeCtx, "Interest", [ result.session.toDate(), result.time ]);
            });
        }
    };

    this.handleResetData = () => {
        for (let bridgeId in this.bridgeCtxs) {
            this.sendBridgeMessageSafe(this.bridgeCtxs[bridgeId], "ResetSampleBuffer", []);
        }
    };

    this.handleInputGeneralSample = (sample) => {
        for (let bridgeId in this.bridgeCtxs) {
            let bridgeCtx = this.bridgeCtxs[bridgeId];
            let fullProtocol = sample.fullProtocol;
            if (bridgeCtx.listeningGeneralSampleFlags[fullProtocol] == null) continue;
            this.sendBridgeMessageSafe(bridgeCtx, "GeneralSample", [ fullProtocol, sample.protocol, sample.channel, sample.session.toDate(), sample.time, sample.values ]);
        }
    };

    this.handleWindowMessage = (msgData) => {
        if (msgData == null || msgData instanceof Array == false) return;
        if (msgData.length < 3 || msgData[0] != "BIBridgeMessage") return;
        
        let bridgeId = msgData[1];
        let bridgeCtx = this.bridgeCtxs[bridgeId];
        if (bridgeCtx == null) return;

        let funcId = msgData[2];
        let params = msgData.slice(3);
        switch (funcId) {
            case "ClientInitialized":
                bridgeCtx.clientInitialized = true;
                break;
            case "Print":
                appoxCallback.onDebugMessage(params[0], "BIBridge");
                break;
            case "Alert":
                appoxCallback.onAlert(params[0], params[1]);
                break;
            case "Confirm":
                appoxCallback.onConfirm(params[1], params[2]).then((confirmed) => {
                    this.sendBridgeMessage(bridgeCtx, "Confirm", [ params[0], confirmed ]);
                });
                break;
            case "Log":
                switch (params[1]) {
                    case 1: // BILogLevel.Info
                        appoxCallback.onLog(params[0], appoxEnums.LogLevel.Info);
                        break;
                    case 3: // BILogLevel.Error
                        appoxCallback.onLog(params[0], appoxEnums.LogLevel.Error);
                        break;
                    case 2: // BILogLevel.Warning
                        appoxCallback.onLog(params[0], appoxEnums.LogLevel.Warning);
                        break;
                }
                break;
            case "ViewConfig":
                bridgeCtx.viewConfig = params[0];
                break;
            case "ListenGeneralSample":
                for (let protocol of params[0]) {
                    let fullProtocol = protocol + (params[1] == null ? "" : "@" + params[1]);
                    bridgeCtx.listeningGeneralSampleFlags[fullProtocol] = true;
                }
                break;
            case "QueryAudioDriversInfo":
                this.handleQueryAudioDriversInfo(bridgeCtx);
                break;
            case "QueryBusDevicesInfo":
                this.handleQueryBusDevicesInfo(bridgeCtx);
                break;
            case "QueryBusMessageInfo":
                this.handleQueryBusMessageInfo(bridgeCtx, params);
                break;
            case "QueryBusProtocolFileChannel":
                this.handleQueryBusProtocolFileChannel(bridgeCtx, params);
                break;
            case "QueryBusProtocolInfo":
                this.handleQueryBusProtocolInfo(bridgeCtx, params);
                break;
            case "QueryChannelNames":
                this.handleQueryChannelNames(bridgeCtx, params);
                break;
            case "QueryGenerationList":
                this.handleQueryGenerationList(bridgeCtx);
                break;
            case "QueryGlobalParameter":
                this.handleQueryGlobalParameter(bridgeCtx, params);
                break;
            case "QueryGlobalVariable":
                this.handleQueryGlobalVariable(bridgeCtx, params);
                break;
            case "QueryPythonModuleConfig":
                this.handleQueryPythonModuleConfig(bridgeCtx, params);
                break;
            case "QueryPythonTemplateInfo":
                this.handleQueryPythonTemplateInfo(bridgeCtx, params);
                break;
            case "QuerySessionList":
                this.handleQuerySessionList(bridgeCtx, params);
                break;
            case "QuerySignalInfo":
                this.handleQuerySignalInfo(bridgeCtx, params);
                break;
            case "QuerySignalsInBusMessage":
                this.handleQuerySignalsInBusMessage(bridgeCtx, params);
                break;
            case "QueryVideoDevicesInfo":
                this.handleQueryVideoDevicesInfo(bridgeCtx);
                break;
            case "QueryVideoJpeg":
                this.handleQueryVideoJpeg(bridgeCtx, params);
                break;
            case "RunStandaloneTask":
                this.handleRunStandaloneTask(bridgeCtx, params);
                break;
            case "SelectBusMessage":
                this.handleSelectBusMessage(bridgeCtx, params);
                break;
            case "SelectBusMessages":
                this.handleSelectBusMessages(bridgeCtx, params);
                break;
            case "SelectBusProtocols":
                this.handleSelectBusProtocols(bridgeCtx, params);
                break;
            case "SelectSignal":
                this.handleSelectSignal(bridgeCtx, params);
                break;
            case "SelectSignals":
                this.handleSelectSignals(bridgeCtx, params);
                break;
            case "SetGlobalParameter":
                this.handleSetGlobalParameter(params);
                break;
            case "SetGlobalVariable":
                this.handleSetGlobalVariable(params);
                break;
            case "SetModuleConfig":
                this.handleSetModuleConfig(params);
                break;
            case "SetPythonModuleConfig":
                this.handleSetPythonModuleConfig(params);
                break;
            case "ConfigDataEncryption":
                this.handleConfigDataEncryption();
                break;
        }
    };

    this.handleQueryAudioDriversInfo = (bridgeCtx) => {
        agencyAsync.getAudioDrivers().then((srcDrivers) => {
            if (srcDrivers == null) {
                this.sendBridgeMessage(bridgeCtx, "AudioDriversInfo", []);
                return;
            }
            let dstDrivers = [];
            let promises = [];
            for (let i = 0; i < srcDrivers.length; i++) {
                let index = i;
                let srcDriver = srcDrivers[index];
                dstDrivers.push({id: srcDriver.driverID, name: srcDriver.driverName, recDevs: [], repDevs: []});
                promises.push(agencyAsync.getAudioRecordDevices(srcDriver.driverID).then((srcDevices) => {
                    srcDevices.forEach((srcDevice) => {
                        dstDrivers[index].recDevs.push({id: srcDevice.deviceID, name: srcDevice.deviceName});
                    });
                }));
                promises.push(agencyAsync.getAudioReplayDevices(srcDriver.driverID).then((srcDevices) => {
                    srcDevices.forEach((srcDevice) => {
                        dstDrivers[index].repDevs.push({id: srcDevice.deviceID, name: srcDevice.deviceName});
                    });
                }));
            }
            Promise.all(promises).then(() => {
                this.sendBridgeMessage(bridgeCtx, "AudioDriversInfo", dstDrivers);
            });
        });
    };

    this.handleQueryBusDevicesInfo = (bridgeCtx) => {
        agencyAsync.getBusDevices().then((srcDevices) => {
            if (srcDevices == null) {
                this.sendBridgeMessage(bridgeCtx, "BusDevicesInfo", []);
                return;
            }
            let dstDevices = [];
            srcDevices.forEach((srcDevice) => {
                let dstSupportedTypes = [];
                srcDevice.supportedTypes.forEach((srcType) => {
                    switch (srcType) {
                        case appoxEnums.BusChannelType.Can:
                            dstSupportedTypes.push(1); // BIBusChannelType.Can
                            break;
                        case appoxEnums.BusChannelType.CanFD:
                            dstSupportedTypes.push(2); // BIBusChannelType.CanFD
                            break;
                        case appoxEnums.BusChannelType.Lin:
                            dstSupportedTypes.push(3); // BIBusChannelType.Lin
                            break;
                        case appoxEnums.BusChannelType.Flexray:
                            dstSupportedTypes.push(4); // BIBusChannelType.Flexray
                            break;
                        case appoxEnums.BusChannelType.Ethernet:
                            dstSupportedTypes.push(5); // BIBusChannelType.Ethernet
                            break;
                        case appoxEnums.BusChannelType.SomeIP:
                            dstSupportedTypes.push(6); // BIBusChannelType.SomeIP
                            break;
                    }
                });
                dstDevices.push({ type: srcDevice.id.type, serial: srcDevice.id.serial.toString(), index: srcDevice.id.index, description: srcDevice.description, supported: dstSupportedTypes });
            });
            this.sendBridgeMessage(bridgeCtx, "BusDevicesInfo", dstDevices);
        });
    };

    this.handleQueryBusMessageInfo = (bridgeCtx, params) => {
        let key = params[0];
        let busMessageID = params[1];
        agencyAsync.getBusMessageInfo(busMessageID).then((info) => {
            if (info == null) {
                this.sendBridgeMessage(bridgeCtx, "BusMessageInfo", [ key, null, null, null, null ]);
            }
            else {
                this.sendBridgeMessage(bridgeCtx, "BusMessageInfo", [ key, busMessageID, info.protocolFileID, info.localID, info.messageName ]);
            }
        });
    };

    this.handleQueryBusProtocolFileChannel = (bridgeCtx, params) => {
        let busProtocolFileName = params[0];
        agencyAsync.getBusProtocolFileChannel(busProtocolFileName).then((channel) => {
            this.sendBridgeMessage(bridgeCtx, "BusProtocolFileChannel", [ busProtocolFileName, channel || 0 ]);
        });
    };

    this.handleQueryBusProtocolInfo = (bridgeCtx, params) => {
        let key = params[0];
        let busProtocolIdFileName = params[1];
        let busProtocolIdMD5 = params[2];
        let appoxBusProtocolID = new AppoxBusProtocolFileID(busProtocolIdFileName, busProtocolIdMD5);
        Promise.all([agencyAsync.getBusProtocolFileState(appoxBusProtocolID), agencyAsync.getBusProtocolFileChannel(appoxBusProtocolID.fileName)]).then(([state, channel]) => {
            if (channel == null) channel = 0;
            if (state == null) {
                this.sendBridgeMessage(bridgeCtx, "BusProtocolInfo", [ key, null, null, null, null ]);
            }
            else {
                this.sendBridgeMessage(bridgeCtx, "BusProtocolInfo", [ key, busProtocolIdFileName, busProtocolIdMD5, state, channel ]);
            }
        });
    };

    this.handleQueryChannelNames = (bridgeCtx, params) => {
        let key = params[0];
        let protocol = params[1];
        let channelCount = params[2];
        let protocols = [];
        for (let i = 0; i < channelCount; i++) {
            protocols.push(protocol + "@" + i.toString());
        }
        Promise.all(protocols.map((p) => agencyAsync.getChannelAliasName(p))).then((values) => {
            let names = {};
            for (let i = 0; i < channelCount; i++) {
                names[protocols[i]] = values[i];
            }
            this.sendBridgeMessage(bridgeCtx, "ChannelNames", [ key, names ]);
        });
    };

    this.handleQueryGenerationList = (bridgeCtx) => {
        agencyAsync.getGenerationList().then((list) => {
            this.sendBridgeMessage(bridgeCtx, "GenerationList", [list || []]);
        });
    };

    this.handleQueryGlobalParameter = (bridgeCtx, params) => {
        let id = params[0];
        let defaultValue = params[1];
        agencyAsync.getGlobalParameter(id, defaultValue).then((val) => {
            this.sendBridgeMessage(bridgeCtx, "GlobalParameter", [ id, val ]);
        });
    };

    this.handleQueryGlobalVariable = (bridgeCtx, params) => {
        let id = params[0];
        let defaultValue = params[1];
        agencyAsync.getGlobalVariable(id, defaultValue).then((val) => {
            this.sendBridgeMessage(bridgeCtx, "GlobalVariable", [ id, val ]);
        });
    };

    this.handleQueryPythonModuleConfig = (bridgeCtx, params) => {
        let templateID = params[0];
        let idPostfix = params[1];
        agencyAsync.getModuleConfig(null, "python-scripts.pluginpython").then((configString) => {
            let config = { enable: false, name: "", freq: null, params: {} };

            let parser = new DOMParser();
            let xmlDoc = parser.parseFromString(configString, "text/xml");
            let scripts = xmlDoc.getElementsByTagName("s");

            let found = false;
            for (let i = 0; i < scripts.length; i++) {
                let script = scripts[i];
                let currentTemplateID = script.getAttribute("template");
                if (currentTemplateID !== templateID) continue;
                
                let templatePostfix = script.getAttribute("template_postfix");
                if (templatePostfix !== idPostfix) continue;

                config.enable = script.getAttribute("enabled") === "yes";
                if (appoxCallback.onGetAppLanguage() == appoxEnums.Language.Chinese) {
                    config.name = script.getAttribute("name_zh");
                    if (!config.name) config.name = script.getAttribute("name_ch");
                    if (!config.name) config.name = script.getAttribute("name");
                } else {
                    config.name = script.getAttribute("name");
                }
                
                let overwriteFreq = script.getAttribute("overwrite_freq");
                config.freq = overwriteFreq ? parseInt(overwriteFreq) : null;

                config.params = {};
                let overwriteParams = script.getElementsByTagName("overwrite_param");
                for (let j = 0; j < overwriteParams.length; j++) {
                    let param = overwriteParams[j];
                    let name = param.getAttribute("name");
                    let value = param.getAttribute("value");
                    if (name && value) {
                        config.params[name] = value;
                    }
                }
                
                found = true;
                break;
            }

            this.sendBridgeMessage(bridgeCtx, "PythonModuleConfig", [ templateID, idPostfix, found ? config : null ]);
        });
    };

    this.handleQueryPythonTemplateInfo = (bridgeCtx, params) => {
        let templateID = params[0];
        agencyAsync.getGlobalVariable("Python.Template." + templateID, null).then((infoString) => {
            if (infoString == null) {
                this.sendBridgeMessage(bridgeCtx, "PythonTemplateInfo", [ templateID, null ]);
                return;
            }
            let bytes = new TextEncoder().encode(infoString);
            let base64 = btoa(String.fromCharCode.apply(null, bytes));
            let origin = new TextDecoder().decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)));
            this.sendBridgeMessage(bridgeCtx, "PythonTemplateInfo", [ templateID, base64 ]);
        });
    };

    this.handleQuerySessionList = (bridgeCtx, params) => {
        let filtered = params[0];
        let promise = filtered ? agencyAsync.getFilteredSessionList() : agencyAsync.getSessionList();
        promise.then((srcList) => {
            if (srcList == null) {
                this.sendBridgeMessage(bridgeCtx, "SessionList", [ [], filtered ]);
                return;
            }
            let dstList = [];
            srcList.forEach((srcSession) => {
                dstList.push(srcSession.toDate());
            });
            this.sendBridgeMessage(bridgeCtx, "SessionList", [ dstList, filtered ]);
        });
    };

    this.handleQuerySignalInfo = (bridgeCtx, params) => {
        let key = params[0];
        let signalID = params[1];
        this.signalIDToSignalInfoComps(signalID).then((info) => {
            this.sendBridgeMessage(bridgeCtx, "SignalInfo", [ key, info ]);
        });
    };

    this.handleQuerySignalsInBusMessage = (bridgeCtx, params) => {
        let key = params[0];
        let busMessageID = params[1];
        agencyAsync.getSignalNamesOfBusMessage(busMessageID).then((names) => {
            if (names == null) {
                this.sendBridgeMessage(bridgeCtx, "SignalsInBusMessage", [ key, null ]);
            }
            else {
                this.sendBridgeMessage(bridgeCtx, "SignalsInBusMessage", [ key, names.map((name) => busMessageID + ":" + name) ]);
            }
        });
    };

    this.handleQueryVideoDevicesInfo = (bridgeCtx) => {
        agencyAsync.getVideoDevices().then((devices) => {
            if (devices == null) {
                this.sendBridgeMessage(bridgeCtx, "VideoDevicesInfo", []);
                return;
            }
            let dstDevices = [];
            devices.forEach((srcDevice) => {
                let dstInputModes = [];
                srcDevice.inputModes.forEach((srcMode) => {
                    let dstCodec = srcMode.inputCodec;
                    dstInputModes.push({ codec: dstCodec, width: srcMode.size.width, height: srcMode.size.height });
                });

                let dstOutputModes = [];    
                srcDevice.outputModes.forEach((srcMode) => {
                    let dstCodec = srcMode.outputCodec;
                    dstOutputModes.push({ codec: dstCodec, width: srcMode.size.width, height: srcMode.size.height });
                });

                dstDevices.push({ type: srcDevice.id.type, id: srcDevice.id.localID, info: srcDevice.hardwareInfo, inModes: dstInputModes, outModes: dstOutputModes });
            });
            this.sendBridgeMessage(bridgeCtx, "VideoDevicesInfo", dstDevices);
        });
    };

    this.handleQueryVideoJpeg = async (bridgeCtx, params) => {
        let channel = params[0];
        let sessionDate = params[1];
        let time = params[2];
        let maxGap = params[3];
        let timeline = Number.MAX_VALUE;
        if (sessionDate != null) {
            let appoxSession = new AppoxSessionIdentifier(null);
            appoxSession.fromDate(sessionDate);
            if (!appoxSession.isValid) {
                this.sendBridgeMessage(bridgeCtx, "VideoJpeg", [ null, channel, null, time ]);
                return;
            }
            let sessionTimeline = await agencyAsync.getSessionTimeline(appoxSession);
            if (sessionTimeline == null) {
                this.sendBridgeMessage(bridgeCtx, "VideoJpeg", [ null, channel, sessionDate, time ]);
                return;
            }
            timeline = sessionTimeline + time;
        }
        let result = await agencyAsync.getPreviewJpeg(channel, timeline, maxGap || Number.MAX_VALUE);
        let jpegBytes = result[0];
        if (jpegBytes == null) {
            this.sendBridgeMessage(bridgeCtx, "VideoJpeg", [ null, channel, sessionDate, time ]);
            return;
        }
        let jpegBase64 = btoa(String.fromCharCode.apply(null, jpegBytes));
        let timestamp = result[1];
        this.sendBridgeMessage(bridgeCtx, "VideoJpeg", [ jpegBase64, channel, timestamp.session.toDate(), timestamp.offset ]);
    };

    this.handleRunStandaloneTask = (bridgeCtx, params) => {
        let key = params[0];
        let taskTypeID = params[1];
        let config = params[2];
        agencyAsync.runStandaloneTask(taskTypeID, config).then((result) => {
            let taskResult = result[0] == appoxEnums.TaskResult.TaskClassNotFound ? "TaskTypeNotFound" : result[0];
            let returnValue = result[1];
            this.sendBridgeMessage(bridgeCtx, "RunStandaloneTaskResult", [ key, taskResult, returnValue ]);
        });
    };

    this.handleSelectBusMessage = (bridgeCtx, params) => {
        let key = params[0];
        let originID = params[1];
        appoxCallback.onSelectBusMessage(originID).then((result) => {
            if (result == null) {
                this.sendBridgeMessage(bridgeCtx, "BusMessage", [ key, null, null, null, null ]);
                return;
            }
            agencyAsync.getBusMessageInfo(result).then((info) => {
                this.sendBridgeMessage(bridgeCtx, "BusMessage", [ key, result, info.protocolFileID, info.localID, info.messageName ]);
            });
        });
    };

    this.handleSelectBusMessages = (bridgeCtx, params) => {
        let key = params[0];
        let selected = params[1];
        let availableCount = params[2];
        appoxCallback.onSelectBusMessages(selected, availableCount).then((result) => {
            this.sendBridgeMessage(bridgeCtx, "BusMessages", [ key, result ]);
        });
    };

    this.handleSelectBusProtocols = (bridgeCtx, params) => {
        let key = params[0];
        let selected = params[1];
        appoxCallback.onSelectBusProtocolFiles(selected == null ? [] : selected.map(id => new AppoxBusProtocolFileID(id.name, id.md5))).then((result) => {
            if (result == null) {
                this.sendBridgeMessage(bridgeCtx, "BusProtocols", [ key, null ]);
            }
            else {
                this.sendBridgeMessage(bridgeCtx, "BusProtocols", [ key, result.map(id => {return { name: id.fileName, md5: id.md5 } }) ]);
            }
        });
    };

    this.handleSelectSignal = (bridgeCtx, params) => {
        let key = params[0];
        let originValueID = params[1];
        let withSignBit = params[2];
        let originSignBitID = params[3];
        let withScale = params[4];
        let originScale = params[5];
        let unit = params[6];
        appoxCallback.onSelectSignal(originValueID, withSignBit, originSignBitID, withScale, originScale, unit).then(async (result) => {
            let valueID = result[0];
            let signBitID = result[1];
            let scale = result[2];
            let valueSignalInfo = null;
            let signBitSignalInfo = null;
            if (valueID != null) {
                valueSignalInfo = await this.signalIDToSignalInfoComps(valueID);
                if (valueSignalInfo == null) return;
            }
            if (signBitID != null) {
                signBitSignalInfo = await this.signalIDToSignalInfoComps(signBitID);
                if (signBitSignalInfo == null) return;
            }
            this.sendBridgeMessage(bridgeCtx, "Signal", [ key, valueSignalInfo, signBitSignalInfo, scale ]);
        });
    };

    this.handleSelectSignals = (bridgeCtx, params) => {
        let key = params[0];
        let selected = params[1];
        let availableCount = params[2];
        appoxCallback.onSelectSignals(selected, availableCount).then((result) => {
            this.sendBridgeMessage(bridgeCtx, "Signals", [ key, result ]);
        });
    };

    this.handleSetGlobalParameter = (params) => {
        let id = params[0];
        let value = params[1];
        agencyAsync.setGlobalParameter(id, value);
    };

    this.handleSetGlobalVariable = (params) => {
        let id = params[0];
        let value = params[1];
        agencyAsync.setGlobalVariable(id, value);
    };

    this.handleSetModuleConfig = (params) => {
        let moduleClassID = params[0];
        let config = params[1];
        agencyAsync.setModuleConfig(null, moduleClassID, config);
    };

    this.handleSetPythonModuleConfig = (params) => {
        let templateID = params[0];
        let idPostfix = params[1];
        let config = params[2];
        agencyAsync.getModuleConfig(null, "python-scripts.pluginpython").then((configString) => {

            let parser = new DOMParser();
            let xmlDoc = parser.parseFromString(configString, "text/xml");
            let root = xmlDoc.documentElement;
            let scripts = xmlDoc.getElementsByTagName("s");

            let found = false;
            for (let i = 0; i < scripts.length; i++) {
                let script = scripts[i];
                let currentTemplateID = script.getAttribute("template");
                if (currentTemplateID !== templateID) continue;
                
                let templatePostfix = script.getAttribute("template_postfix");
                if (templatePostfix !== idPostfix) continue;

                script.setAttribute("enabled", config.enabled ? "yes" : "no");
                if (config.moduleName) {
                    script.setAttribute("name", config.moduleName);
                    script.setAttribute("name_zh", config.moduleName);
                }
                if (config.overwriteFreq !== null) {
                    script.setAttribute("overwrite_freq", config.overwriteFreq.toString());
                } else {
                    script.removeAttribute("overwrite_freq");
                }

                let oldParams = script.getElementsByTagName("overwrite_param");
                while (oldParams.length > 0) {
                    oldParams[0].parentNode.removeChild(oldParams[0]);
                }

                for (let key in config.overwriteParameters) {
                    let param = xmlDoc.createElement("overwrite_param");
                    param.setAttribute("name", key);
                    param.setAttribute("value", config.overwriteParameters[key]);
                    script.appendChild(param);
                }
                
                found = true;
                break;
            }

            if (!found) {
                let newScript = xmlDoc.createElement("s");
                newScript.setAttribute("enabled", config.enabled ? "yes" : "no");
                newScript.setAttribute("name", config.moduleName || "Unnamed Template");
                newScript.setAttribute("name_zh", config.moduleName || "未命名模板");
                newScript.setAttribute("template", templateID);
                newScript.setAttribute("template_postfix", idPostfix);
                if (config.overwriteFreq !== null) {
                    newScript.setAttribute("overwrite_freq", config.overwriteFreq.toString());
                }

                for (let key in config.overwriteParameters) {
                    let param = xmlDoc.createElement("overwrite_param");
                    param.setAttribute("name", key);
                    param.setAttribute("value", config.overwriteParameters[key]);
                    newScript.appendChild(param);
                }
                
                root.appendChild(newScript);
            }

            agencyAsync.setModuleConfig(null, "python-scripts.pluginpython", xmlDoc.documentElement.outerHTML);
        });
    };

    this.handleConfigDataEncryption = () => {
        appoxUtility.configDataEncryption();
    };
}

function BIBridgeContext(bridgeId, entryName, viewConfig, relatedModuleList, showHelp) {

    this.bridgeId = bridgeId;
    this.entryName = entryName;
    this.viewConfig = viewConfig;
    this.relatedModuleList = relatedModuleList;
    this.relatedModuleConfigs = null;
    this.queryingRelatedModuleConfigs = false;
    this.showHelp = showHelp;
    this.contentWindow = null;
    this.clientInitialized = false;
    this.initArgsSent = false;
    this.isRunning = null;
    this.isRunningUpdate = null;
    this.interest = null;
    this.interestUpdate = null;
    this.listeningGeneralSampleFlags = {};
}